一文带你看懂Java中的Lock锁底层AQS到底是如何实现的
欢迎点击关注公众号,利用碎片化时间学习,每天进步一点点。
相信大家对Java中的Lock锁应该不会陌生,比如ReentrantLock,锁主要是用来解决解决多线程运行访问共享资源时的线程安全问题。那你是不是很好奇,这些Lock锁api是如何实现的呢?本文就是来探讨一下这些Lock锁底层的AQS(AbstractQueuedSynchronizer)到底是如何实现的。
本文是基于ReentrantLock来讲解,ReentrantLock加锁只是对AQS的api的调用,底层的锁的状态(state)和其他线程等待(Node双向链表)的过程其实是由AQS来维护的
加锁
我们先来看看加锁的过程,先看源码,然后模拟两个线程来加锁的过程。
上图是ReentrantLock的部分实现。里面有一个Sync的内部类的实例变量,这个Sync内部类继承自AQS,Sync子类就包括公平锁和非公平锁的实现。说白了其实ReentrantLock是通过Sync的子类来实现加锁。
我们就来看一下Sync的非公平锁的实现NonfairSync。
重写了它的lock加锁方法,在实现中因为是非公平的,所以一进来会先通过cas尝试将AQS类的state参数改为1,直接尝试加锁。如果尝试加锁失败会调用AQS的acquire方法继续尝试加锁。
假设这里有个线程1先来调用lock方法,那么此时没有人加锁,那么就通过CAS操作,将AQS中的state中的变量由0改为1,代表有人来加锁,然后将加锁的线程设置为自己如图。
那么此时有另一个线程2来加锁,发现通过CAS操作会失败,因为state已经被设置为1了,线程线程2就会设置失败,那么此时就会走else,调用AQS的acquire方法继续尝试加锁。
进入到acquire会先调用tryAcquire再次尝试加锁,而这个tryAcquire方法AQS其实是没有什么实现的,会调用到NonfairSync里面的tryAcquire,而tryAcquire实际会调用到Sync内部类里面的nonfairTryAcquire非公平尝试加锁方法。
先获取锁的状态,判断锁的状态是不是等于0,等于0说明没人加锁,可以尝试去加,如果被加锁了,就会走else if,else if会判断加锁的线程是不是当前线程,是的话就给state 加 1,代表当前线程加了2次锁,就是可重入锁的意思(所谓的可重入就是代表一个线程可以多次获取到锁,只是将state 设置为多次,当线程多次释放锁之后,将state 设置为0才代表当前线程完全释放了锁)。
这里所有的条件假设都不成立。也就是线程2尝试加锁的时候,线程1并没有释放锁,那么这个方法就会返回false。
接下来就会走到addWaiter方法,这个方法很重要,就是将当前线程封装成一个Node,然后将这个Node放入双向链表中。addWaiter先根据指定模式创建指定的node节点,因为ReentrantLock是独占模式,所以传进去的EXCLUSIVE,这里通过当前线程和模式传入,初始化一个双向node节点,获取最后一个节点,根据最后一个节点是否存在来操作当前节点的父级。如果尾节点不存在会去调用enq去初始化
放入链表中之后如图。
然后调用acquireQueued方法
这个方法一进来也会尝试将当前节点去加锁,然后如果加锁成功就将当前节点设置为头节点,最后将当前线程中断,等待唤醒。
线程2进来的时候,刚好线程2的前一个节点是头节点,但是不巧的是调用tryAcquire方法,还是失败,那么此时就会走shouldParkAfterFailedAcquire方法,这个方法是在线程休眠之前调用的,很重要,我们来看看干了什么事。
到这里,释放锁的过程就讲完了,其实很简单,就是当线程完完全全释放了锁,会唤醒当前链表中的没有取消的,离头结点最近的节点(一般就是链表中的第二个节点),然后被唤醒的节点就会获取到锁,将头节点设置为自己。
总结
相信看完这篇文章,大家对AQS的底层有了更深层次的了解。AQS其实就是内部维护一个锁的状态变量state和一个双向链表,加锁成功就将state的值加1,加锁失败就将自己当前线程放入链表的尾部,然后休眠,等待其他线程完完全全释放锁之后将自己唤醒,唤醒之后会尝试加锁,加锁成功就会执行业务代码了。
到这里本文就结束了,如果你有什么疑问欢迎私信告诉我。
如果觉得这篇文章对你有所帮助,还请帮忙点赞、在看、转发一下,码字不易,非常感谢!
往期热门文章推荐